/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.camel.maven.packaging;

import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.apache.camel.tooling.model.BaseOptionModel;
import org.apache.camel.tooling.util.FileUtil;
import org.apache.camel.tooling.util.PackageHelper;
import org.apache.camel.tooling.util.Strings;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.runtime.RuntimeInstance;
import org.codehaus.plexus.build.BuildContext;

public abstract class AbstractGeneratorMojo extends AbstractMojo {

    public static final String GENERATED_MSG = "Generated by camel build tools - do NOT edit this file!";
    public static final String NL = "\n";

    private static final Map<String, Class<?>> KNOWN_CLASSES_CACHE = new ConcurrentHashMap<>();
    private static final RuntimeInstance VELOCITY = createVelocityRuntime();
    private static final Map<String, Template> VELOCITY_TEMPLATES = new ConcurrentHashMap<String, Template>();

    /**
     * The maven project.
     */
    @Parameter(property = "project", required = true, readonly = true)
    protected MavenProject project;

    /**
     * Maven ProjectHelper.
     */
    @Component
    protected MavenProjectHelper projectHelper;

    /**
     * build context to check changed files and mark them for refresh (used for m2e compatibility)
     */
    @Component
    protected BuildContext buildContext;

    private DynamicClassLoader projectClassLoader;

    static {
        KNOWN_CLASSES_CACHE.put("Byte", Byte.class);
        KNOWN_CLASSES_CACHE.put("Boolean", Boolean.class);
        KNOWN_CLASSES_CACHE.put("Date", Date.class);
        KNOWN_CLASSES_CACHE.put("Double", Double.class);
        KNOWN_CLASSES_CACHE.put("Duration", Duration.class);
        KNOWN_CLASSES_CACHE.put("String", String.class);
        KNOWN_CLASSES_CACHE.put("Integer", Integer.class);
        KNOWN_CLASSES_CACHE.put("Long", Long.class);
        KNOWN_CLASSES_CACHE.put("File", File.class);
        KNOWN_CLASSES_CACHE.put("Object", Object.class);
        KNOWN_CLASSES_CACHE.put("int", int.class);
        KNOWN_CLASSES_CACHE.put("long", long.class);
        KNOWN_CLASSES_CACHE.put("boolean", boolean.class);
    }

    public void execute(
            MavenProject project,
            MavenProjectHelper projectHelper,
            BuildContext buildContext)
            throws MojoFailureException, MojoExecutionException {
        this.project = project;
        this.projectHelper = projectHelper;
        this.buildContext = buildContext;
        execute();
    }

    protected void addResourceDirectory(Path path) {
        projectHelper.addResource(project, path.toString(), Collections.singletonList("**/*"), Collections.emptyList());
    }

    public void refresh(Path file) {
        refresh(buildContext, file);
    }

    protected String velocity(String templatePath, Map<String, Object> ctx) {
        VelocityContext context = new VelocityContext(ctx);
        Template template = VELOCITY_TEMPLATES.computeIfAbsent(templatePath, VELOCITY::getTemplate);

        StringWriter writer = new StringWriter();
        template.merge(context, writer);
        return writer.toString();
    }

    private static RuntimeInstance createVelocityRuntime() {
        Properties props = new Properties();
        props.setProperty("resource.loaders", "class");
        props.setProperty("resource.loader.class.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
        RuntimeInstance velocity = new RuntimeInstance();
        velocity.init(props);
        return velocity;
    }

    protected boolean updateResource(Path dir, String fileName, String data) {
        boolean updated;
        updated = updateResource(buildContext, dir.resolve(fileName), data);
        if (!fileName.endsWith(".java")) {
            Path outputDir = Paths.get(project.getBuild().getOutputDirectory());
            updated |= updateResource(buildContext, outputDir.resolve(fileName), data);
        }
        return updated;
    }

    protected String createProperties(String key, String val) {
        return createProperties(project, key, val);
    }

    public static String createProperties(MavenProject project, String key, String val) {
        String data;
        StringBuilder properties = new StringBuilder(256);
        properties.append("# ").append(GENERATED_MSG).append(NL);
        properties.append(key).append("=").append(val).append(NL);
        properties.append("groupId=").append(project.getGroupId()).append(NL);
        properties.append("artifactId=").append(project.getArtifactId()).append(NL);
        properties.append("version=").append(project.getVersion()).append(NL);
        properties.append("projectName=").append(project.getName()).append(NL);
        if (project.getDescription() != null) {
            properties.append("projectDescription=").append(project.getDescription()).append(NL);
        }

        String annotations = project.getProperties().getProperty("annotations");
        if (!Strings.isNullOrEmpty(annotations)) {
            properties.append("annotations=").append(annotations).append(NL);
        }

        data = properties.toString();
        return data;
    }

    public static void refresh(BuildContext buildContext, Path file) {
        if (buildContext != null) {
            buildContext.refresh(file.toFile());
        }
    }

    public static boolean updateResource(BuildContext buildContext, Path out, String data) {
        try {
            if (FileUtil.updateFile(out, data)) {
                refresh(buildContext, out);
                return true;
            }
        } catch (IOException e) {
            throw new IOError(e);
        }
        return false;
    }

    public static boolean haveResourcesChanged(Log log, MavenProject project, BuildContext buildContext, String suffix) {
        String baseDir = project.getBasedir().getAbsolutePath();
        for (Resource r : project.getBuild().getResources()) {
            File file = new File(r.getDirectory());
            if (file.isAbsolute()) {
                file = new File(r.getDirectory().substring(baseDir.length() + 1));
            }

            if (log.isDebugEnabled()) {
                String path = file.getPath() + "/" + suffix;
                log.debug("Checking  if " + path + " (" + r.getDirectory() + "/" + suffix + ") has changed.");
            }
            if (buildContext.hasDelta(new File(file, suffix))) {
                if (log.isDebugEnabled()) {
                    log.debug("Indeed " + suffix + " has changed.");
                }
                return true;
            }
        }
        return false;
    }

    protected static <T> Supplier<T> cache(Supplier<T> supplier) {
        return new Supplier<>() {
            T value;

            @Override
            public T get() {
                if (value == null) {
                    value = supplier.get();
                }
                return value;
            }
        };
    }

    protected Class<?> loadClass(String loadClassName) {
        return KNOWN_CLASSES_CACHE.computeIfAbsent(loadClassName, k -> doLoadClass(loadClassName));
    }

    private Class<?> doLoadClass(String loadClassName) {
        Class<?> optionClass;
        String org = loadClassName;
        while (true) {
            try {
                optionClass = getProjectClassLoader().loadClass(loadClassName);
                break;
            } catch (ClassNotFoundException e) {
                int dotIndex = loadClassName.lastIndexOf('.');
                if (dotIndex == -1) {
                    if (getLog().isDebugEnabled()) {
                        getLog().debug("Failed to load class: " + loadClassName);
                    }

                    throw new NoClassDefFoundError(org);
                } else {
                    loadClassName = loadClassName.substring(0, dotIndex) + "$" + loadClassName.substring(dotIndex + 1);
                    if (getLog().isDebugEnabled()) {
                        getLog().debug("Relocating previous class name for loading as: " + loadClassName);
                    }
                }
            }
        }
        return optionClass;
    }

    protected final DynamicClassLoader getProjectClassLoader() {
        if (projectClassLoader == null) {
            try {
                projectClassLoader = DynamicClassLoader.createDynamicClassLoader(project.getCompileClasspathElements());
            } catch (DependencyResolutionRequiredException e) {
                throw new RuntimeException("Unable to create project classloader", e);
            }
        }
        return projectClassLoader;
    }

    protected boolean isJsonFile(Path p, BasicFileAttributes a) {
        return a.isRegularFile() && p.toFile().getName().endsWith(PackageHelper.JSON_SUFIX);
    }

    @SuppressWarnings("unused") // use by velocity templates
    public String canonicalClassName(String className) {
        return Strings.canonicalClassName(className);
    }

    @SuppressWarnings("unused") // use by velocity templates
    public String format(String fmt, Object... args) {
        return String.format(fmt, args);
    }

    @SuppressWarnings("unused") // use by velocity templates
    public TreeSet<?> newTreeSet() {
        return new TreeSet<>();
    }

    @SuppressWarnings("unused") // use by velocity templates
    public Set<BaseOptionModel> findConfigurations(Collection<? extends BaseOptionModel> options) {
        final Set<String> found = new HashSet<>();
        return options.stream()
                .filter(o -> o.getConfigurationField() != null)
                .filter(o -> found.add(o.getConfigurationClass()))
                .collect(Collectors.toCollection(LinkedHashSet::new));
    }
}
